package com.ervanalb.cellradio.server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

import com.ervanalb.cellradio.client.Informational;
import com.ervanalb.cellradio.server.ServerSocket.ServerSocketSource;
import com.ervanalb.cellradio.shared.communicator.Communicator;
import com.ervanalb.cellradio.shared.communicator.QueuedListener;
import com.ervanalb.cellradio.shared.communicator.ResponseListener;
import com.ervanalb.cellradio.shared.communicator.SocketSink;
import com.ervanalb.cellradio.shared.communicator.packets.ConnectedPacketData;
import com.ervanalb.cellradio.shared.communicator.packets.Packet;
import com.ervanalb.cellradio.shared.communicator.packets.PacketData;

/**
 * The main server loop. Instantiating this creates a new server thread.
 * Asynchronous commands may be sent to it.
 * 
 * The server instantiates a Communicator which listens on channel 0. When it
 * receives connect requests, it creates new ClientPresence objects to handle
 * communication with the client. Received messages (from all clients) go to the
 * listener, which is polled in the main loop.
 * 
 * @author eric
 */
public class ServerMain extends Thread {
	public ServerSocket ss;
	public SocketSink sink;
	public int num;

	protected Communicator firstComm;

	protected QueuedListener listener;

	protected boolean go;

	public Map<Integer, ClientPresence> clients;

	public ServerMain(int listenPort, int sendPort) throws SocketException {
		ss = new ServerSocket(listenPort);
		sink = new SocketSink(sendPort);
		listener = new QueuedListener();
		clients = new HashMap<Integer, ClientPresence>();

		num = 1;

		// Add listener for clients that don't yet have an ID
		ServerSocketSource sss = ss.addClient(0);
		firstComm = new Communicator(sss, sink, listener);
		start();
	}

	public void run() {
		Packet p;

		go = true;

		while (go) {
			try {
				p = listener.pop(100);
				if (p == null)
					continue;

				if (p.channel == 0) {
					if (p.pd.type == PacketData.TYPE_CONNECT) {
						Informational<Boolean> i = addClient(p);
						log("Conect", i);
					} else if (p.pd.type == PacketData.TYPE_ERROR) {
						log("Received error on channel zero: "
								+ p.pd.parseError());
					} else {
						log("Error: Received unknown command on channel zero");
					}
				} else if (clients.containsKey(p.channel)) {
					ClientPresence client = clients.get(p.channel);

					if (p.pd.type == PacketData.TYPE_ERROR) {
						log("Received error from client " + client + ": "
								+ p.pd.parseError().message);
						disconnect(client);
					} else if (p.pd.type == PacketData.TYPE_DISCONNECT) {
						log("Received disconnect from client " + client);
						cutoff(client);
					} else if (p.pd.type == PacketData.TYPE_AUTHENTICATE) {
						Informational<Boolean> i = authenticate(client, p);
						log("Authenticate", client, i);
					}
				}

			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

		ss.finish();

		firstComm.finish();

		for (int id : clients.keySet()) {
			try {
				disconnect(clients.get(id));
			} catch (IOException e) {
				// In this catch block we give zero fucks
			}
		}
	}

	public void log(String message) {
		System.out.println(message);
	}

	public void log(String action, Informational<Boolean> i) {
		log(action + ": " + (i.result ? "Success" : i.message));
	}

	public void log(String action, ClientPresence client,
			Informational<Boolean> i) {
		log(action + " " + client + ": " + (i.result ? "Success" : i.message));
	}

	public void finish() {
		go = false;
		interrupt();
	}

	public Informational<Boolean> addClient(Packet connectPacket)
			throws IOException {

		int id = num++;

		ServerSocketSource sss = ss.addClient(id);
		Communicator c = new Communicator(sss, sink, listener);
		ClientPresence cp = new ClientPresence(c, id, connectPacket.addr);
		clients.put(id, cp);

		Informational<Boolean> i = sendConnectedMessage(id, connectPacket);
		if (!i.result)
			return i;

		i = cp.sendHello();
		if (!i.result) {
			disconnect(cp);
			return i;
		}
		cp.cs.connected.set(true);
		return i;
	}

	public void disconnect(ClientPresence client) throws IOException {
		client.sendDisconnect();
		cutoff(client);
	}

	public void cutoff(ClientPresence client) {
		client.cutoff();
		clients.remove(client.id);
	}

	public Informational<Boolean> authenticate(ClientPresence client, Packet p)
			throws IOException {
		Informational<Boolean> i = client.sendAuthenticated((byte) 1, p.number);
		if (!i.result) {
			disconnect(client);
			return i;
		}
		client.cs.username.set(p.pd.parseAuthenticate().username);
		client.cs.authenticated.set(true);
		return i;
	}

	public Informational<Boolean> sendConnectedMessage(int id,
			Packet connectPacket) throws IOException {
		PacketData pd = new ConnectedPacketData(id).pack();
		Packet p = new Packet(0, 0, connectPacket.number, pd,
				connectPacket.addr);
		Informational<ResponseListener> i = firstComm.safeSend(p, false);
		if (i == null) // i being completely null indicates success
			return new Informational<Boolean>(true);
		else
			return new Informational<Boolean>(
					"Failed to send connected message: " + i.message);
	}

	public Informational<Boolean> sendErrorMessage(Packet erroneousPacket,
			String error) throws IOException {
		PacketData pd = new ConnectedPacketData(erroneousPacket.channel).pack();
		Packet p = new Packet(0, 0, erroneousPacket.number, pd,
				erroneousPacket.addr);
		Informational<ResponseListener> i = firstComm.safeSend(p, false);
		if (i == null) // i being completely null indicates success
			return new Informational<Boolean>(true);
		else
			return new Informational<Boolean>("Failed to send error message: "
					+ i.message);
	}

	public static void main(String[] arg) throws IOException,
			InterruptedException {
		ServerMain s = new ServerMain(4003, 4002);

		System.out.println("Server running.");
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		String str;
		for (;;) {
			str = br.readLine();
			if (str.equals("quit")) {
				s.finish();
				break;
			} else {
				System.out.println("Unrecognized command.");
			}
		}
		System.out.println("Goodbye.");
	}
}
